iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0
Modern Web

TypeScript 魔法 - 喚醒你的程式碼靈感系列 第 12 篇

Day12 - TypeScript 的秘密武器(下) - 介面(Interfaces)

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20230927/20152047Cmwtpgbm0r.png

什麼是介面?

介面是一種用於定義型別結構的抽象概念。它們提供了一種方法來描述物件的形狀,指定它們應該具有哪些屬性和方法。換句話說,介面定義了一個物件應該是什麼樣子,但不關心具體的實現細節。

介面使用範例

在以下的例子中,我們使用 interface 關鍵字來定義了一個介面叫做 Person。然後,我們宣告了一個名為 person 的變數,並明確指定了它的型別為 Person。這樣做的目的是約束 person 物件的形狀必須與 Person 介面中所定義的一致。

interface Person {
  name: string;
  age: number;
  greet: (phrase: string) => void;
}

const person: Person = {
  name: '肉鬆',
  age: 30,
  greet: (phrase: string) => {
    console.log(`我是${person.name} - ${phrase}`); // 我是肉鬆 - 一起學習 TypeScript
  },
};

person.greet('一起學習 TypeScript');

當定義的變數比介面少了一些屬性(少了 age 屬性):

interface Person {
  name: string;
  age: number;
  greet: (phrase: string) => void;
}

const person: Person = { // TypeScript 報錯
  name: '肉鬆',
  greet: (phrase: string) => {
    console.log(`我是${person.name} - ${phrase}`); // 我是肉鬆 - 一起學習 TypeScript
  },
};

person.greet('一起學習 TypeScript');

https://ithelp.ithome.com.tw/upload/images/20230927/20152047VU7P3cNncp.png

當定義的變數比介面多了一些屬性(多了 gender 屬性):

interface Person {
  name: string;
  age: number;
  greet: (phrase: string) => void;
}

const person: Person = {
  name: '肉鬆',
  age: 30,
  gender: 'male', // TypeScript 報錯
  greet: (phrase: string) => {
    console.log(`我是${person.name} - ${phrase}`); // 我是肉鬆 - 一起學習 TypeScript
  },
};

person.greet('一起學習 TypeScript');

https://ithelp.ithome.com.tw/upload/images/20230927/20152047aMNTP4NFGE.png

唯讀屬性(readonly)

當定義的介面中屬性加上 readonly 表示該屬性被建立後不可被修改(userId 屬性):

interface Person {
  readonly userId: number;
  name: string;
  age: number;
}

const person: Person = {
  userId: 123,
  name: '肉鬆',
  age: 30,
};

person.userId = 456; // TypeScript 報錯

https://ithelp.ithome.com.tw/upload/images/20230927/20152047SNEhyG4yTV.png

可選屬性(?)

當定義的介面中屬性加上 ? 表示該屬性可以存在,也可以不存在。

age 屬性不存在:

interface Person {
  name: string;
  age?: number;
}

const person: Person = {
  name: '肉鬆',
};

age 屬性存在:

interface Person {
  name: string;
  age?: number;
}

const person: Person = {
  name: '肉鬆',
  age: 30,
};

這時仍然不允許新增未定義的屬性(多了 gender 屬性):

const person: Person = {
  name: '肉鬆',
  age: 30,
  gender: 'male', // TypeScript 報錯
};

索引屬性

允許我們定義一個介面,其中可以包含不固定名稱的屬性,並指定它的型別。

當索引簽名型別為 any:
表示我們可以在物件中包含任何屬性,這些屬性的值可以是任何型別,包括數字、字串、布林、物件、函式等。

interface ErrorContainer1 {
  id: number;
  [key: string]: any;
}

const errorBag1: ErrorContainer1 = {
  id: 123,
  email: '無效的信箱格式',
  isActive: true,
};

當索引簽名型別為 string:
表示我們可以在物件包含任何屬性,但這些屬性的值必須都是字串型別。
特別注意:一旦定義任意屬性的型別,確定屬性與可選屬性的型別都必須是該任意屬性型別的子集。

interface ErrorContainer2 {
  id: number; // TypeScript 報錯,類型 'number' 的屬性 'id' 不可指派給 'string' 索引類型 'string'
  [key: string]: string;
}

const errorBag2: ErrorContainer2 = {
  id: '123',
  email: '無效的信箱格式',
  age: 30, // TypeScript 報錯,類型 'number' 不可指派給類型 'string'
};

讓我們修改程式碼以解決 TypeScript 錯誤:

interface ErrorContainer2 {
  id: string;
  [key: string]: string;
}

const errorBag2: ErrorContainer2 = {
  id: '0123',
  email: '無效的信箱格式',
  age: '30',
};

繼承(extends)

我們可以使用 extends 關鍵字將一個介面繼承到另一個介面,從而擴展介面的屬性。

interface Person {
  name: string;
  age: number;
}

interface User extends Person {
  employeeId: string;
}

const user: User = {
  name: '肉鬆',
  age: 30,
  employeeId: '123',
};

在上方的程式碼中,user 物件被定義為符合 User 介面,而 User 介面則繼承了 Person 介面。這意味著 user 物件必須包含 User 介面中的所有屬性,以及由 Person 介面繼承的 name 和 age 這兩個屬性。

資料定義中的 type 與 interface 合併

  1. 使用 type 關鍵字建立一個名為 Gender 的型別別名。這個型別別名被設定為只能取 male 或 female 的字串值。
  2. 在 Person 介面中,gender 屬性的型別被設定為我們剛剛定義的型別別名 Gender。
  3. 建立一個 person 物件,符合 Person 介面,此時,如果嘗試將 gender 屬性的值設定為除了 male 或 female 之外的字串值,TypeScript 將會產生錯誤。
type Gender = 'male' | 'female';

interface Person {
  name: string;
  age: number;
  gender: Gender;
}

const person: Person = {
  name: '肉鬆',
  age: 30,
  gender: 'male', // 若嘗試賦予 male 或 female 外的字串值,TypeScript 會報錯
};

type 與 interface 差異

談到 TypeScript 中的型別定義時,主要有兩個選擇:type 和 interface。以下是它們之間的主要差異,以便更清楚地理解它們:

Type(型別)

  • 使用 type 來定義型別。
  • 常用於聯合型別、交集型別和模板文字型別。
  • 使用 & 和 | 運算符來組合多個型別。
  • 可命名基本型別、自訂型別、元組、聯合型別等。
  • 可使用 typeof 獲取變數的型別。
  • 可使用泛型(Generics)來創建可重用的型別。
  • 不支持擴展(extends)關鍵字用於類別。
  • 常用於描述具體的值和資料結構。

Interface(介面)

  • 使用 interface 來定義介面。
  • 常用於描述物件的形狀(Shape)。
  • 可用於擴展其他介面。
  • 支持多重繼承(一個介面可以繼承多個介面)。
  • 可包含方法的定義,包括方法名稱、參數和返回值的型別。
  • 不能命名基本型別、聯合型別、交集型別等,僅用於描述物件結構。
  • 不支持 typeof 用於變數,而僅用於類別、函數和命名空間。
  • 常用於描述物件的屬性和方法結構,並且可以實現多態性。

本日重點

  1. 命名規範:以大寫字母開頭,有助於與普通變數或函式區分。
  2. 一個物件符合某個介面時,物件的形狀(屬性名稱和型別)必須與介面一致。
  3. readonly 表示該屬性被建立後不可被修改。
  4. ? 表示該屬性可以存在,也可以不存在。
  5. 當索引簽名的型別為 any 時,屬性的值可以是任何型別。反之,如果定義了任意屬性型別,其他屬性和可選屬性的型別必須是該任意屬性型別的子集。
  6. 使用 extends 關鍵字可以擴展一個介面的屬性。

參考


上一篇
Day11 - TypeScript 的秘密武器(上) - 型別別名(Type Aliases)
下一篇
Day13 - 解析函式返回型別的精隨
系列文
TypeScript 魔法 - 喚醒你的程式碼靈感 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言